/**
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.api.adwords.starterkit.examples.enhancedads;

import com.google.api.adwords.starterkit.examples.enhancedads.db.Catalog;
import com.google.api.adwords.starterkit.examples.enhancedads.db.Category;

import org.apache.log4j.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

/**
 * XML2DB class loads the products available at a generic XML file 
 * and store its content into the EnhancedAds DB. The XML2DB class
 * should map the tags in this generic XML into variables that the
 * application can understand. These mapping rules are defined at the
 * settings.xml file. An example is available at this package in 
 * settings.xml file - tag EnhancedAdWords. EnhancedAds do not translate
 * one XML format to another, like XSLT, to after that process its content.
 * The process works on-the-fly.
 * 
 * @author rodrigo.f.vale@gmail.com
 */
public class XML2DB extends DefaultHandler {
  private SAXParserFactory factory;
  private SAXParser saxParser;

  private String delimiterTag;
  private String nameTag;
  private String priceTag;
  private String regularPriceTag;
  private String prodIdTag;
  private String catIdTag;
  private String catNameTag;
  private String instalmentsTag;
  private String instalmentsValueTag;
  private String unitsTag;
  private String urlTag;
  private String authorTag;
  private String imgTag;
  private String otherTag;

  private String nameValue;
  private String priceValue = "0.0";
  private String regularPriceValue = "0.0";
  private String prodIdValue;
  private String catIdValue;
  private String catNameValue;
  private String instalments = "0";
  private String instalmentsValue = "0.0";
  private String unitsValue = "0";
  private String urlValue;
  private String authorValue;
  private String imgValue;
  private String otherValue;
  private HashSet<String> categories = new HashSet<String>(); 
  
  private ArrayList<Collection> collections;
  
  private Category category;
  private Catalog catalog;
  private Currency currency;

  private String regexp;
  private int regexpInstalmentsGroup;
  private int regexpInstalmentsValueGroup;

  private String value;

  private Stack<String> tags = new Stack<String>();

  private int batch = 1; 
  
  private static final Logger log = Logger.getLogger("StarterKit");

  /**
   * The XML2DB constructor receives all mapping tags as parameter.
   * Each variable understood by EnhancedAds is mapped into the XML 
   * that contains the catalog of products. For example, EnhancedAds
   * understand the variable Price. This variable should be present
   * at the product XML. A map between the tag at the XML that 
   * represents the price and EnhancedAds is defined in the priceTag
   * param in settings.xml file. For a better understand of it, take a look at 
   * the settings.xml file in this package. 
   */
  public XML2DB(Connection conn, String delimiterTag, String nameTag, 
      String priceTag, String regularPriceTag, String prodIdTag, 
      String catIdTag, String catNameTag, String instalmentsTag, String regexp,
      int regexpInstalmentsGroup, int regexpInstalmentsValueGroup, 
      String instalmentsValueTag, String unitsTag, String urlTag, 
      String authorTag, String imgTag, String otherTag, Currency currency, 
      ArrayList<Collection> collections) {

    this.delimiterTag = delimiterTag;
    this.nameTag = nameTag;    
    this.priceTag = priceTag;
    this.regularPriceTag = regularPriceTag;
    this.prodIdTag = prodIdTag;
    this.catIdTag = catIdTag;
    this.catNameTag = catNameTag;
    this.instalmentsTag = instalmentsTag;
    this.instalmentsValueTag = instalmentsValueTag;
    this.unitsTag = unitsTag;
    this.urlTag = urlTag;
    this.authorTag = authorTag;
    this.imgTag = imgTag;
    this.otherTag = otherTag;
    
    this.regexp = regexp;
    this.regexpInstalmentsGroup = regexpInstalmentsGroup;
    this.regexpInstalmentsValueGroup = regexpInstalmentsValueGroup;
    
    this.collections = collections;
    
    catalog = new Catalog(conn);
    category = new Category(conn);
    
    this.currency = currency;

    factory = SAXParserFactory.newInstance();
    try {
      saxParser = factory.newSAXParser();
    } catch (ParserConfigurationException e) {
      e.printStackTrace();
    } catch (SAXException e) {
      e.printStackTrace();
    }
  }

  /**
   * Entry point to process the XML from advertiser  
   * 
   */
  public void parser(File file) {
    log.info("Parsing the XML of products");
    try {
      resetValue();
      saxParser.parse(file, this);
    } catch (SAXException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  /**
   * While processing the XML all tags are stored in a stack. This method
   * return the last tag in the stack with it parents tags.
   */
  public String getTag(){
    String fTag = "";
    Iterator<String> i = tags.iterator();
    while (i.hasNext()) {
      String tag = i.next();
      if (fTag.isEmpty()) {
        fTag = tag;
      } else {
        fTag = fTag.concat(".").concat(tag);
      }
    }
    return fTag;
  }

  /**
   * Compares two tags from XML to see if one tag is child of the other.
   * Example: the tag products.product.categoryid is child of the tag
   * products.product
   */
  public boolean isChild(String tag1, String tag2) {
    String[] tags1 = tag1.split("\\.");
    String[] tags2 = tag2.split("\\.");

    int x = 0;

    while ((x < tags1.length) && (x < tags2.length)) {
      if (!tags1[x].equals(tags2[x])) {
        return false;
      }
      x++;
    }
    return true;
  }

  /**
   * It is time to load a new product. This method reset the values
   * of the variables from the last product processed. However, not all
   * values should be reseted. Example, if the XML of products has a 
   * category tag and inside of this category is the list of products,
   * this method should not reset the categoryId value because it is
   * common for all products until the next category tag is defined 
   * in the XML.
   */
  public void resetValue() {

    // the delimiterTag defines the Tag in the XML that represents a
    // product. If the nameTag (that represents the name of the product)
    // is a chils of the delimiterTag, this means that its value can be 
    // reseted. The class XML2DB is able to convert any kind of XML structure
    // 
    if (isChild(nameTag, delimiterTag) || nameTag.isEmpty()) {
      nameValue = "";
    }

    if (isChild(priceTag, delimiterTag) || priceTag.isEmpty()) {
      priceValue = "0.0";
    }

    if (isChild(regularPriceTag, delimiterTag) || regularPriceTag.isEmpty()) {
      regularPriceValue = "0.0";
    }

    if (isChild(prodIdTag, delimiterTag) || prodIdTag.isEmpty()) {
      prodIdValue = "";
    }

    if (isChild(catIdTag, delimiterTag) || catIdTag.isEmpty()) {
      catIdValue = "";      
    }    

    if (isChild(instalmentsTag, delimiterTag) || instalmentsTag.isEmpty()) {
      instalments = "0";
    }

    if (isChild(instalmentsValueTag, delimiterTag) || instalmentsValueTag.isEmpty()) {
      instalmentsValue = "0.0";
    }

    if (isChild(unitsTag, delimiterTag) || unitsTag.isEmpty()) {
      unitsValue = "0";
    }

    if (isChild(catNameTag, delimiterTag) || catNameTag.isEmpty()) {
      catNameValue = "";
    }

    if (isChild(urlTag, delimiterTag) || urlTag.isEmpty()) {
      urlValue = "";
    }

    if (isChild(authorTag, delimiterTag) || authorTag.isEmpty()) {
      authorValue = "";
    }

    if (isChild(imgTag, delimiterTag) || imgTag.isEmpty()) {
      imgValue = "";
    }

    if (isChild(otherTag, delimiterTag) || otherTag.isEmpty()) {
      otherValue = "";
    }

    Iterator<Collection> i = collections.iterator();
    while (i.hasNext()) {
      Collection collection = i.next();
      ArrayList<CollectionItem> items = collection.getItems();
      Iterator<CollectionItem> ii = items.iterator();
      while (ii.hasNext()) {        
        CollectionItem collectionItem = ii.next();
        String tag = collectionItem.getTag();
        if (isChild(tag, delimiterTag)) {
          collectionItem.setValue(null);
        }        
      }
    }
  }

  /**
   * Store the variables understood by the application in the DB.
   */
  public void storeValues() {
    float regularPrice = currency.toFloat(regularPriceValue);
    float price = currency.toFloat(priceValue);
    int discount = (int) Math.floor((1 - (price / regularPrice)) * 100);
    int parcel = parseInt(instalments, 0);
    float parcelValue = currency.toFloat(instalmentsValue);
    int units = parseInt(unitsValue, 0);
    
    catalog.saveBatch(prodIdValue, nameValue, price, regularPrice, urlValue, 
        imgValue, catIdValue, parcel, parcelValue, authorValue, otherValue, 
        units, discount);
    
    if (!categories.contains(catIdValue)) {
      category.saveBatch(catIdValue, catNameValue, price);
      categories.add(catIdValue);
    }
    
    Iterator<Collection> i = collections.iterator();
    while (i.hasNext()) {
      Collection collection = i.next();
      String collectionValue = getCollectionValue(collection);      
      if (collectionValue != null) {
        catalog.saveBatch(prodIdValue, nameValue, price, regularPrice, urlValue, 
            imgValue, collectionValue, parcel, parcelValue, authorValue, 
            otherValue, units, discount);
        if (!categories.contains(collectionValue)) {
          category.saveBatch(collectionValue, collection.getName(), price);
          categories.add(collectionValue);
        }
      }
    }
    batch++;
    
    if ((batch % 10000) == 0) {
      log.debug("Starting to dump to DB");
      catalog.executeBatch();
      category.executeBatch();
      log.debug("Processed " + batch + " products (total) from catalog");
    }
  }
  
  /**
   * Collections is an advanced way to deal with categories. A category is in 
   * general a single TAG, normally "CATEGORYID" in the XML. A Collection could
   * be a set of TAGs with specific filters. This method return the value
   * of a collection. The value is a String concated with the values of each
   * TAG that belongs to the collection.
   */
  public String getCollectionValue(Collection collection) {
    String collectionValue = "";
    ArrayList<CollectionItem> collectionItems = collection.getItems();    
    
    for (int i = 0; i < collectionItems.size(); i++) {
      CollectionItem item = collectionItems.get(i);
      
      // if any item from the collection is null, means that this collection
      // is not valid.
      if (item.getValue() == null) {
        return null;
      } else {
        if (!item.isHidden()) {
          if (collectionValue.length() > 0) {
            collectionValue = collectionValue + "+";
          }
          collectionValue = collectionValue + item.getValue();
        }
      }
    }    
    
    if (collectionValue.length() > 0) {
      collectionValue = collection.getName() + ":" + collectionValue;
    } else {
      collectionValue = collection.getName();
    }

    return collectionValue;
  }

  /**
   * After reading a tag content from XML, it is time to verify if its value
   * was mapped to an internal variable used by EnhancedAds. if so, the tag 
   * value is assigned to the internal variable. 
   */
  public void setValue(String tag, String value) {
    // the delimiterTag defines when a products definition starts at the XML
    if (tag.equals(delimiterTag)) {
      storeValues();
      resetValue();
    } else if (tag.equals(priceTag)) {
      priceValue = value;        
      if (regularPriceTag.isEmpty()) {
        regularPriceValue = value;
      }
    } else if (tag.equals(regularPriceTag)) {
      regularPriceValue = value;        
      if (priceTag.isEmpty()) {
        regularPriceValue = value;
      }
    } else if (tag.equals(prodIdTag)) {
      prodIdValue = value;        
    } else if (tag.equals(catIdTag)) {
      catIdValue = value;        
    } else if (tag.equals(instalmentsTag)) {
      // some advertisers defines the parcel in a single tag in the XML like
      // 12 x $50.99". We need to separate these values. In the settings.xml
      // file we can define a regexp that does that.
      if (!regexp.isEmpty()) {
        Pattern p = Pattern.compile(regexp);
        Matcher m = p.matcher(value);
        if (m.matches()) {
          instalments = m.group(regexpInstalmentsGroup);
          instalmentsValue = m.group(regexpInstalmentsValueGroup);
        }
      } else {            
        instalments = value;
      }
    } else if (tag.equals(instalmentsValueTag)) {
      instalmentsValue = value;        
    } else if (tag.equals(nameTag)) {
      nameValue = value;        
    } else if (tag.equals(unitsTag)) {
      unitsValue = value;        
    } else if (tag.equals(catNameTag)) {
      catNameValue = value;        
    } else if (tag.equals(urlTag)) {
      urlValue = value;        
    } else if (tag.equals(authorTag)) {
      authorValue = value;        
    } else if (tag.equals(imgTag)) {
      imgValue = value;        
    } else if (tag.equals(otherTag)) {
      otherValue = value;        
    }
    
    // Aggregator can use tags from others elements, that is why it is
    // out of the 'else if' structure
    Iterator<Collection> i = collections.iterator();
    while (i.hasNext()) {
      Collection collection = i.next();
      ArrayList<CollectionItem> collectionItems = collection.getItems();
      for (CollectionItem collectionItem : collectionItems) {
        if (collectionItem.getTag().equals(tag)) {
          collectionItem.setValue(value);
        }
      }
    }
  }

  @Override
  public void startElement(String uri, String localName,
      String qName, Attributes attributes) {

    if ((qName.toLowerCase().equals("xml") || 
        qName.toLowerCase().equals("rss")) 
        && (tags.size() == 0)) {
      return;
    }

    tags.push(qName);

    int l = attributes.getLength();
    for (int x = 0; x < l; x++) {
      String name = attributes.getQName(x);
      String value = attributes.getValue(x);

      tags.push(name);
      String tag = getTag();
      setValue(tag, value);
      tags.pop();
    }      
  }

  @Override
  public void endElement(String uri, String localName,
      String qName) {
    if ((qName.toLowerCase().equals("xml") || 
        qName.toLowerCase().equals("rss")) && (tags.size() == 0)) {
      return;
    }
    String tag = getTag();
    setValue(tag, value);
    tags.pop();
  }

  @Override
  public void characters(char ch[], int start, int length) {
    value = new String(ch, start, length);
  }
  
  @Override
  public void endDocument() {
    catalog.executeBatch();
    category.executeBatch();
    category.setLowerPrice();
    category.setLowerParcel();
    category.setMaxDiscount();    
    category.setItems();
  }

  private int parseInt(String value, int defaultValue) {
      try {
        value = value.replaceAll("[^0123456789]", "");
        return Integer.parseInt(value);
      }
      catch(NumberFormatException e) {
        log.warn(e.getMessage() + " - Using the " + defaultValue + 
            " as default value");
      }
      return defaultValue;
  }
}